/*
 * Copyright (c) 2008, 2015, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the Classpath exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package org.openjdk.btrace.core.aggregation;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

/**
 * BTrace stores the results of aggregating functions in an Aggregation. The aggregated values may
 * be grouped using a composite {@link AggregationKey}.
 *
 * <p>
 *
 * @author Christian Glencross
 */
public class Aggregation implements Cloneable {

  private static final AggregationKey NULL_AGGREGATION_KEY = new AggregationKey(new Object[0]);
  private final AggregationFunction type;
  private final ConcurrentHashMap<AggregationKey, AggregationValue> values =
      new ConcurrentHashMap<>();

  /**
   * Creates an aggregation.
   *
   * @param type the type of aggregation function to use
   */
  public Aggregation(AggregationFunction type) {
    this.type = type;
  }

  /**
   * Adds an item of data to the aggregation with an empty key. This method is recommended if the
   * aggregation will contain only a single value.
   *
   * @param data the value to be added
   */
  public void add(long data) {
    add(NULL_AGGREGATION_KEY, data);
  }

  /**
   * Adds an item of data to the aggregation with the specified grouping key.
   *
   * @param key the aggregation key
   * @param data the value to be added
   */
  public void add(AggregationKey key, long data) {
    AggregationValue aggregationValue = values.get(key);
    if (aggregationValue == null) {
      aggregationValue = type.newValue();
      AggregationValue existing = values.putIfAbsent(key, aggregationValue);
      if (existing != null) {
        aggregationValue = existing;
      }
    }
    aggregationValue.add(data);
  }

  /** Resets all values in the aggregation to their default. */
  public void clear() {
    for (AggregationValue value : values.values()) {
      value.clear();
    }
  }

  /**
   * Reduces the size of the aggregation to the absolute value of <code>count</code>. If count is
   * greater than zero, the largest aggregated values are preserved. If it is less than zero, the
   * smallest aggregated values are preserved. Passing a value of zero clears the aggregation
   * completely.
   *
   * @param count the absolute number indicates the number of aggregated values to preserve.
   */
  public void truncate(int count) {
    if (count == 0) {
      values.clear();
    } else {
      List<Map.Entry<AggregationKey, AggregationValue>> sortedContents = sort();

      int collectionSize = sortedContents.size();
      int numberToRemove = collectionSize - Math.abs(count);
      if (numberToRemove < 0) {
        return;
      }
      List<Map.Entry<AggregationKey, AggregationValue>> removeContents;
      if (count > 0) {
        // Remove from the start of the list
        removeContents = sortedContents.subList(0, numberToRemove);
      } else {
        removeContents = sortedContents.subList(collectionSize - numberToRemove, collectionSize);
      }
      for (int i = 0; i < removeContents.size(); i++) {
        values.remove(removeContents.get(i).getKey());
      }
    }
  }

  /**
   * Returns details of the aggregation in a tabular format which can be serialized across the wire
   * and formatted for display. The data is represented as a List of rows. The last element in each
   * row represents the aggregated value, the elements before this in the row contain the elements
   * of the aggregating key.
   *
   * @return details of the aggregation in a tabular format.
   */
  public List<Object[]> getData() {
    List<Entry<AggregationKey, AggregationValue>> sortedContents = sort();
    List<Object[]> result = new ArrayList<>(sortedContents.size());

    for (Entry<AggregationKey, AggregationValue> item : sortedContents) {

      Object[] keyElements = item.getKey().getElements();
      int rowSize = keyElements.length + 1;

      Object[] row = new Object[rowSize];
      System.arraycopy(keyElements, 0, row, 0, keyElements.length);
      row[rowSize - 1] = item.getValue().getData();
      result.add(row);
    }

    return result;
  }

  /**
   * Returns a list of the AggregationKeys that belong to this aggregation.
   *
   * @return a list of aggregationsKeys belonging to this aggregation.
   */
  public List<AggregationKey> getKeyData() {
    List<AggregationKey> keyList = new ArrayList<>();
    List<Entry<AggregationKey, AggregationValue>> sortedContents = sort();
    for (Entry<AggregationKey, AggregationValue> item : sortedContents) {
      keyList.add(item.getKey());
    }

    return keyList;
  }

  /**
   * Returns a value for the given key if the key has a value associated with it. Returns zero if
   * the key is not valid for this Aggregation.
   *
   * @param key
   * @return the value for the given key, or zero.
   */
  public Long getValueForKey(AggregationKey key) {
    AggregationValue aggregationValue = values.get(key);
    if (aggregationValue != null) {
      return aggregationValue.getValue();
    } else {
      return 0L;
    }
  }

  /**
   * @return a list of key/value pairs contained in this aggregation by sorted by ascending value.
   */
  private List<Map.Entry<AggregationKey, AggregationValue>> sort() {
    ArrayList<Map.Entry<AggregationKey, AggregationValue>> result =
        new ArrayList<>(values.entrySet());
    Collections.sort(
        result,
        new Comparator<Map.Entry<AggregationKey, AggregationValue>>() {

          @Override
          public int compare(
              Entry<AggregationKey, AggregationValue> o1,
              Entry<AggregationKey, AggregationValue> o2) {
            long i1 = o1.getValue().getValue();
            long i2 = o2.getValue().getValue();
            if (i1 < i2) {
              return -1;
            } else if (i1 == i2) {
              return 0;
            } else {
              return 1;
            }
          }
        });
    return result;
  }

  @Override
  protected Object clone() throws CloneNotSupportedException {
    return new Aggregation(type);
  }
}
